package com.ht.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.ht.shortlink.project.common.convention.exception.ClientException;
import com.ht.shortlink.project.common.convention.exception.ServiceException;
import com.ht.shortlink.project.common.enums.validDateTypeEnum;
import com.ht.shortlink.project.config.GotoDomainWhiteListConfiguration;
import com.ht.shortlink.project.dao.entity.*;
import com.ht.shortlink.project.dao.mapper.*;
import com.ht.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.ht.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.ht.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.ht.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.ht.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.ht.shortlink.project.dto.resp.*;
import com.ht.shortlink.project.mq.producer.DelayShortLinkStatsProducer;
import com.ht.shortlink.project.mq.producer.ShortLinkStatsSaveProducer;
import com.ht.shortlink.project.service.ILinkTodayStatsService;
import com.ht.shortlink.project.service.IShortLinkService;
import com.ht.shortlink.project.toolkit.HashUtil;
import com.ht.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import static com.ht.shortlink.project.common.constant.RedisCacheConstant.*;
import static com.ht.shortlink.project.common.enums.ShortLinkErrorCode.SHORT_LINK_NOT_EXIST;

/**
 *  短链接接口实现层
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements IShortLinkService {
    private final RBloomFilter<String> createLinkCachePenetrationBloomFilter;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkTodayStatsMapper linkTodayStatsMapper;
    private final ILinkTodayStatsService linkTodayStatsService;
    private final DelayShortLinkStatsProducer delayShortLinkStatsProducer;
    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;
    private final ShortLinkStatsSaveProducer shortLinkStatsSaveProducer;



    @Value("${short-link.domain.default}")
    private String createLinkDefaultDomain;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public ShortLinkCreateRespDTO create(ShortLinkCreateReqDTO requestParam) {
        whiteListCheck(requestParam.getOriginUrl());
        // 1、通过原始链接生成短链接
        String shortLinkSuffix = generateLinkSuffix(requestParam);
        // 2、构建完整实体类
        String fullShortUrl = StrBuilder
                .create(createLinkDefaultDomain)
                .append("/")
                .append(shortLinkSuffix)
                .toString();
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(createLinkDefaultDomain)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .shortUri(shortLinkSuffix)
                .enableStatus(0)
                .fullShortUrl(fullShortUrl)
                .favicon(getFavicon(requestParam.getOriginUrl()))
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .delTime(0L)
                .build();
        ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();

        try {
            save(shortLinkDO);
            Db.save(shortLinkGotoDO);
        } catch (DuplicateKeyException ex){
            // 3、若插入失败，则说明shortUri已存在
            throw new ServiceException(String.format("短链接 %s 生成重复,请稍后再试", fullShortUrl));

        }
        createLinkCachePenetrationBloomFilter.add(fullShortUrl);
        stringRedisTemplate.opsForValue().set(
                String.format(SHORT_LINK_GOTO_KEY, fullShortUrl),
                shortLinkDO.getOriginUrl(),
                LinkUtil.getTimeStamp(shortLinkDO.getValidDate()),
                TimeUnit.MILLISECONDS
        );
        // 4、构建响应对象返回
        return ShortLinkCreateRespDTO.builder()
                .gid(requestParam.getGid())
                .fullShortUrl("http://" + fullShortUrl)
                .originUrl(requestParam.getOriginUrl())
                .build();
    }

    private void whiteListCheck(String originUrl) {
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        if(enable == null || !enable){
            return;
        }

        if(StrUtil.isBlank(originUrl)){
            throw new ClientException("原始链接填写错误");
        }

        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        String domain = LinkUtil.extractDomain(originUrl);
        if(CollUtil.isEmpty(details) || !details.contains(domain)){
            throw new ClientException(String.format("该原始链接不在白名单上，请生成以下网站的跳转链接 %s", gotoDomainWhiteListConfiguration.getNames()));
        }
    }


    @Override
    public ShortLinkBatchCreateRespDTO batchCreate(ShortLinkBatchCreateReqDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        List<ShortLinkBaseInfoRespDTO> linkBaseInfoRespDTOS = new ArrayList<>();

        for (int i = 0; i < originUrls.size(); i++) {
            try {
                ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
                shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
                shortLinkCreateReqDTO.setDescribe(describes.get(i));
                ShortLinkCreateRespDTO shortLinkCreateRespDTO = create(shortLinkCreateReqDTO);
                linkBaseInfoRespDTOS.add(ShortLinkBaseInfoRespDTO.builder()
                                .fullShortUrl(shortLinkCreateRespDTO.getFullShortUrl())
                                .describe(describes.get(i))
                                .originUrl(shortLinkCreateRespDTO.getOriginUrl())
                                .build());
            } catch (Throwable  ex){
                log.error("短链接批量创建失败{}", originUrls.get(i));
            }
        }

        return ShortLinkBatchCreateRespDTO.builder()
                .baseLinkInfos(linkBaseInfoRespDTOS)
                .total(linkBaseInfoRespDTOS.size())
                .build();
    }

    @Override
    public IPage<ShortLinkPageRespDTO> queryPage(ShortLinkPageReqDTO requestParam) {
        IPage<ShortLinkDO> resultPage = baseMapper.queryPage(requestParam);
        return resultPage.convert(each -> (BeanUtil.toBean(each, ShortLinkPageRespDTO.class)));
    }

    @Override
    public List<ShortLinkGroupCountRespDTO> listGroupLinkCount(List<String> requestParam) {
        QueryWrapper<ShortLinkDO> queryWrapper = new QueryWrapper<>();
        queryWrapper.select("gid", "count(*) as shortLinkCount")
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .eq("del_time", 0L)
                .in("gid", requestParam)
                .groupBy("gid");

        List<Map<String, Object>> results = baseMapper.selectMaps(queryWrapper);
        List<ShortLinkGroupCountRespDTO> dtoResults = results.stream().map(map -> {
            ShortLinkGroupCountRespDTO dto = new ShortLinkGroupCountRespDTO();
            dto.setGid((String) map.get("gid"));
            dto.setShortLinkCount(((Long) map.get("shortLinkCount")).intValue());
            return dto;
        }).collect(Collectors.toList());

        return dtoResults;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void update(ShortLinkUpdateReqDTO requestParam) {
        whiteListCheck(requestParam.getOriginUrl());
        // 1、根据originGid从数据库查找shortLinkDO
        ShortLinkDO hasShortLinkDO = lambdaQuery().eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0)
                .one();
        // 2、为空，则抛出异常
        if(hasShortLinkDO == null){
            throw new ClientException(SHORT_LINK_NOT_EXIST);
        }
        // 3.1、不为空，构建实体
        // 3.2、判断shortLinkDO的gid与请求参数的gid是否相同
        if(ObjectUtil.equal(hasShortLinkDO.getGid(), requestParam.getGid())){
            // 3.3、相同，则修改
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getDelTime, 0L)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .set(ShortLinkDO::getOriginUrl, requestParam.getOriginUrl())
                    .set(ShortLinkDO::getValidDateType, requestParam.getValidDateType())
                    .set(ShortLinkDO::getValidDate, (ObjectUtil.equal(requestParam.getValidDateType(), validDateTypeEnum.PERMANENT.getType())) ? null : requestParam.getValidDate());
            update(updateWrapper);
        }else{
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_SHORT_LINK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));
            RLock rLock = readWriteLock.writeLock();
            if (!rLock.tryLock()) {
                throw new ServiceException("短链接正在被访问，请稍后修改");
            }
            try{
                // 3.4、不相同，则把原来的记录删了
                LambdaUpdateWrapper<ShortLinkDO> deleteWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0)
                        .set(ShortLinkDO::getDelFlag, 1)
                        .set(ShortLinkDO::getDelTime, System.currentTimeMillis());
                update(deleteWrapper);
                // 3.5、新增短链接
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .gid(requestParam.getGid())
                        .domain(hasShortLinkDO.getDomain())
                        .shortUri(hasShortLinkDO.getShortUri())
                        .fullShortUrl(requestParam.getFullShortUrl())
                        .originUrl(requestParam.getOriginUrl())
                        .clickNum(hasShortLinkDO.getClickNum())
                        .enableStatus(hasShortLinkDO.getEnableStatus())
                        .createdType(hasShortLinkDO.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate((ObjectUtil.equal(requestParam.getValidDateType(), validDateTypeEnum.PERMANENT.getType())) ? null : requestParam.getValidDate())
                        .describe(hasShortLinkDO.getDescribe())
                        .favicon(hasShortLinkDO.getFavicon())
                        .totalPv(hasShortLinkDO.getTotalPv())
                        .totalUv(hasShortLinkDO.getTotalUv())
                        .totalUip(hasShortLinkDO.getTotalUip())
                        .todayPv(hasShortLinkDO.getTodayPv())
                        .todayUv(hasShortLinkDO.getTodayUv())
                        .todayUip(hasShortLinkDO.getTodayUip())
                        .delTime(0L)
                        .build();
                shortLinkDO.setCreateTime(hasShortLinkDO.getCreateTime());
                save(shortLinkDO);

                LambdaQueryWrapper<LinkTodayStatsDO> statsTodayQueryWrapper = Wrappers.lambdaQuery(LinkTodayStatsDO.class)
                        .eq(LinkTodayStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkTodayStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkTodayStatsDO::getDelFlag, 0);
                List<LinkTodayStatsDO> linkStatsTodayDOList = linkTodayStatsMapper.selectList(statsTodayQueryWrapper);
                if (CollUtil.isNotEmpty(linkStatsTodayDOList)) {
                    linkTodayStatsMapper.deleteBatchIds(linkStatsTodayDOList.stream()
                            .map(LinkTodayStatsDO::getId)
                            .toList()
                    );
                    linkStatsTodayDOList.forEach(each -> each.setGid(requestParam.getGid()));
                    linkTodayStatsService.saveBatch(linkStatsTodayDOList);
                }
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkGotoDO::getGid, hasShortLinkDO.getGid());
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
                shortLinkGotoMapper.deleteById(shortLinkGotoDO.getId());
                shortLinkGotoDO.setGid(requestParam.getGid());
                shortLinkGotoMapper.insert(shortLinkGotoDO);
                LambdaUpdateWrapper<LinkAccessStatsDO> linkAccessStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessStatsDO.class)
                        .eq(LinkAccessStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessStatsDO::getDelFlag, 0);
                LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessStatsMapper.update(linkAccessStatsDO, linkAccessStatsUpdateWrapper);
                LambdaUpdateWrapper<LinkLocaleStatsDO> linkLocaleStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkLocaleStatsDO.class)
                        .eq(LinkLocaleStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkLocaleStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkLocaleStatsDO::getDelFlag, 0);
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkLocaleStatsMapper.update(linkLocaleStatsDO, linkLocaleStatsUpdateWrapper);
                LambdaUpdateWrapper<LinkOsStatsDO> linkOsStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkOsStatsDO.class)
                        .eq(LinkOsStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkOsStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkOsStatsDO::getDelFlag, 0);
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkOsStatsMapper.update(linkOsStatsDO, linkOsStatsUpdateWrapper);
                LambdaUpdateWrapper<LinkBrowserStatsDO> linkBrowserStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkBrowserStatsDO.class)
                        .eq(LinkBrowserStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkBrowserStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkBrowserStatsDO::getDelFlag, 0);
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkBrowserStatsMapper.update(linkBrowserStatsDO, linkBrowserStatsUpdateWrapper);
                LambdaUpdateWrapper<LinkDeviceStatsDO> linkDeviceStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkDeviceStatsDO.class)
                        .eq(LinkDeviceStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkDeviceStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkDeviceStatsDO::getDelFlag, 0);
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkDeviceStatsMapper.update(linkDeviceStatsDO, linkDeviceStatsUpdateWrapper);
                LambdaUpdateWrapper<LinkNetworkStatsDO> linkNetworkStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkNetworkStatsDO.class)
                        .eq(LinkNetworkStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkNetworkStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkNetworkStatsDO::getDelFlag, 0);
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkNetworkStatsMapper.update(linkNetworkStatsDO, linkNetworkStatsUpdateWrapper);
                LambdaUpdateWrapper<LinkAccessLogsDO> linkAccessLogsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessLogsDO.class)
                        .eq(LinkAccessLogsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessLogsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessLogsDO::getDelFlag, 0);
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessLogsMapper.update(linkAccessLogsDO, linkAccessLogsUpdateWrapper);
            } finally {
                rLock.unlock();
            }
            // 3.5.1、 TODO 新的分组不存在，则抛出异常

        }
        // 处理缓存
        if(!ObjectUtil.equals(hasShortLinkDO.getValidDateType(), requestParam.getValidDateType()) || ObjectUtil.equals(hasShortLinkDO.getValidDate(), requestParam.getValidDate())){
            stringRedisTemplate.delete(String.format(SHORT_LINK_GOTO_KEY, hasShortLinkDO.getFullShortUrl()));
            if(ObjectUtil.equals(requestParam.getValidDateType(), validDateTypeEnum.PERMANENT) ||
                    (ObjectUtil.equals(requestParam.getValidDateType(), validDateTypeEnum.CUSTOM) && requestParam.getValidDate().before(new Date()))){
                stringRedisTemplate.delete(String.format(SHORT_LINK_GOTO_IS_NULL_KEY, hasShortLinkDO.getFullShortUrl()));
            }
        }
    }

    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !ObjectUtil.equals(each, 80))
                .map(Object::toString)
                .map(each -> ":" + each)
                .orElse("");
        String fullShortUrl = request.getServerName() + serverPort + "/" + shortUri;
        String shortLinkGotoKey = String.format(SHORT_LINK_GOTO_KEY, fullShortUrl);
        String originUrl = stringRedisTemplate.opsForValue().get(shortLinkGotoKey);

        // 解决缓存穿透问题
        boolean contains = createLinkCachePenetrationBloomFilter.contains(fullShortUrl);
        if(!contains){
            ((HttpServletResponse)response).sendRedirect("/page/notfound");
            return;
        }
        if(StrUtil.isNotBlank(stringRedisTemplate.opsForValue().get(String.format(SHORT_LINK_GOTO_IS_NULL_KEY, fullShortUrl)))){
            ((HttpServletResponse)response).sendRedirect("/page/notfound");
            return;
        }

        if(StrUtil.isNotBlank(originUrl)){
            ShortLinkStatsRecordDTO statsRecord = buildStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(null, statsRecord);
            ((HttpServletResponse)response).sendRedirect(originUrl);
            return;
        }
        RLock lock = redissonClient.getLock(String.format(LOCK_SHORT_LINK_GOTO_KEY, fullShortUrl));
        try {
            if(lock.tryLock()){
                originUrl = stringRedisTemplate.opsForValue().get(shortLinkGotoKey);
                if(StrUtil.isNotBlank(originUrl)){
                    ShortLinkStatsRecordDTO statsRecord = buildStatsRecordAndSetUser(fullShortUrl, request, response);
                    shortLinkStats(null, statsRecord);
                    ((HttpServletResponse)response).sendRedirect(originUrl);
                    return;
                }
                ShortLinkGotoDO linkGotoDO = Db.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl)
                        .one();

                if (linkGotoDO == null) {
                    // 设置空缓存
                    stringRedisTemplate.opsForValue().setIfAbsent((String.format(SHORT_LINK_GOTO_IS_NULL_KEY + fullShortUrl)), "-", 30l, TimeUnit.MINUTES);
                    ((HttpServletResponse)response).sendRedirect("/page/notfound");
                    return;
                }
                ShortLinkDO shortLinkDO = lambdaQuery()
                        .eq(ShortLinkDO::getGid, linkGotoDO.getGid())
                        .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                        .eq(ShortLinkDO::getEnableStatus, 0)
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .one();
                if(shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))){
                    // 过期则设置空缓存
                    stringRedisTemplate.opsForValue().setIfAbsent((String.format(SHORT_LINK_GOTO_IS_NULL_KEY + fullShortUrl)), "-", 30l, TimeUnit.MINUTES);
                    ((HttpServletResponse)response).sendRedirect("/page/notfound");
                    return;
                }
                stringRedisTemplate.opsForValue().set(
                        shortLinkGotoKey,
                        shortLinkDO.getOriginUrl(),
                        LinkUtil.getTimeStamp(shortLinkDO.getValidDate()),
                        TimeUnit.MILLISECONDS
                );
                ShortLinkStatsRecordDTO statsRecord = buildStatsRecordAndSetUser(fullShortUrl, request, response);
                shortLinkStats(shortLinkDO.getGid(), statsRecord);
                ((HttpServletResponse)response).sendRedirect(shortLinkDO.getOriginUrl());
            }
        } finally {
            lock.unlock();
        }
    }

    private ShortLinkStatsRecordDTO buildStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        AtomicBoolean uvFlag = new AtomicBoolean();
        AtomicBoolean uipFlag = new AtomicBoolean();
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        AtomicReference<String> uv = new AtomicReference<>();
        // 设置 cookie 值
        Runnable addCookieTask = () -> {
            uv.set(UUID.randomUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            stringRedisTemplate.opsForSet().add(String.format(SHORT_LINK_STATS_UV_KEY, fullShortUrl), uv.get());
            uvFlag.set(Boolean.TRUE);
            ((HttpServletResponse) response).addCookie(uvCookie);
        };

        if(ArrayUtil.isNotEmpty(cookies)){
            Arrays.stream(cookies)
                    .filter(each -> ObjectUtil.equals("uv", each.getName()))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        Long addUv = stringRedisTemplate.opsForSet().add(String.format(SHORT_LINK_STATS_UV_KEY, fullShortUrl), each);
                        uvFlag.set(addUv != null && addUv > 0);
                        uv.set(each);
                    }, addCookieTask);
        } else {
            addCookieTask.run();
        }
        // 获取真实ip
        String actualIpAddress = LinkUtil.getClientIpAddress((HttpServletRequest) request);
        Long addUip = stringRedisTemplate.opsForSet().add(String.format(SHORT_LINK_STATS_UIP_KEY, fullShortUrl), actualIpAddress);
        uipFlag.set(addUip != null && addUip > 0);

        String os = LinkUtil.getOs((HttpServletRequest) request);
        String browser = LinkUtil.getBrowser((HttpServletRequest) request);
        String device = LinkUtil.getDevice((HttpServletRequest) request);
        String network = LinkUtil.getNetwork((HttpServletRequest) request);

        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .remoteAddr(actualIpAddress)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .uv(uv.get())
                .uvFirstFlag(uvFlag.get())
                .uipFirstFlag(uipFlag.get())
                .build();
    }

    public void shortLinkStats(String gid, ShortLinkStatsRecordDTO statsRecord){
        Map<String, String> producerMap = new HashMap<>();
        producerMap.put("gid", gid);
        producerMap.put("statsRecord", JSON.toJSONString(statsRecord));
        shortLinkStatsSaveProducer.send(producerMap);
    }

    private String generateLinkSuffix(ShortLinkCreateReqDTO requestParam) {
        int customShortLinkCount = 0;
        String shortUri;
        while(true){
            if(customShortLinkCount > 10){
                throw new ServiceException("短链接创建频繁，请稍后重试");
            }
            String uuid = UUID.randomUUID().toString();
            shortUri = HashUtil.hashToBase62(requestParam.getOriginUrl() + uuid);
            if(!createLinkCachePenetrationBloomFilter.contains(createLinkDefaultDomain + "/" + shortUri)){
                break;
            }
            customShortLinkCount++;
        }

        return shortUri;
    }

    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }
}
